/**
 * Licensed under the Apache License Version 2.0.
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package net.interknit.knitter.cg.model;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.List;

import net.interknit.knitter.cg.CGMain;
import net.interknit.knitter.cg.util.CGLogger;
import net.interknit.knitter.cg.util.ReflectionHelper;
import net.interknit.knitter.cg.util.Utils;

/**
 *
 */
public class JavaMethod
{
	public enum JavaMethodType
	{
		GETTER, SETTER, QUERY, FUNCTION
	}

	private JavaType javaType;
	private Method method;
	private String id;
	private JavaMethodType methodType = JavaMethodType.FUNCTION;
	private JavaType returnJavaType;
	private final List<JavaType> parameterJavaTypes = new ArrayList<JavaType>();
	private String propertyName; // applicable only if this is an accessor method
	private boolean valid = false;

	public JavaMethod(JavaType javaType, Method m)
	{
		this.javaType = javaType;
		this.method = m;

		String mn = m.getName();
		Type rt = m.getGenericReturnType();
		Type[] ptArr = m.getGenericParameterTypes();

		//
		// validate the method. if validation fails, do not proceed
		//
		if(! validate()) return;

		if(ReflectionHelper.hasOverloadedMethod(javaType.getTypeClass(), mn))
		{
			// use method's length as the unique identifier
			id = mn + m.toGenericString().length();
		}
		else id = mn;

		//
		// for getter:
		// - seed type methods are not considered getters
		// - method name must begin with 'get'
		// - character immediately following 'get' prefix must be an uppercase letter
		// - method name cannot be equal to 'get' (length must be > 3)
		// - method must return a non-void type
		// - method must take exactly 0 parameter
		//
		boolean validGetterMethod =
			! javaType.isSeedType() &&
			(
					mn.startsWith("get") && mn.length() > 3 && mn.charAt(3) >= 'A' && mn.charAt(3) <= 'Z' &&
					rt != null && ! "void".equals(rt.toString()) && ptArr.length == 0
			);

		if(validGetterMethod)
		{
			methodType = JavaMethodType.GETTER;
			propertyName = Utils.derivePropertyName(mn);
		}

		//
		// for setter:
		// - seed type methods are not considered setters
		// - method name must begin with 'set'
		// - character immediately following 'set' prefix must be an uppercase letter
		// - method name cannot be equal to 'set' (length must be > 3)
		// - method must return a void type
		// - method must take exactly 1 parameter
		//
		boolean validSetterMethod =
			! javaType.isSeedType() &&
			(
					mn.startsWith("set") && mn.length() > 3 && mn.charAt(3) >= 'A' && mn.charAt(3) <= 'Z' &&
					(rt == null || "void".equals(rt.toString())) && ptArr.length == 1
			);

		if(validSetterMethod)
		{
			methodType = JavaMethodType.SETTER;
			propertyName = Utils.derivePropertyName(mn);
		}

		//
		// for query:
		// - seed type methods are not considered query methods
		// - method name must being with 'is'
		// - character immediately following 'is' prefix must be an uppercase letter
		// - method name cannot be equal to 'is' (length must be > 2)
		// - method must return a boolean type
		// - method must take exactly 0 parameter
		//
		boolean validQueryMethod =
			! javaType.isSeedType() &&
			(
					mn.startsWith("is") && mn.length() > 2 && mn.charAt(2) >= 'A' && mn.charAt(2) <= 'Z' &&
					rt != null && ("boolean".equals(rt.toString()) || "java.lang.Boolean".equals(rt.toString())) &&
					ptArr.length == 0
			);

		if(validQueryMethod)
		{
			methodType = JavaMethodType.QUERY;
			propertyName = Utils.derivePropertyName(mn);
		}
	}

	private boolean validate()
	{
		List<String> excludedTypes = CGMain.getInstance().getCGConfig().getExcludedTypes();

		for(String excludedType : excludedTypes)
		{
			// validate the return types
			if(ReflectionHelper.getRootComponentClass(
					method.getReturnType()).getName().equals(excludedType))
			{
				return (valid = false);
			}

			// validate the parameter types
			for(Class<?> ptClass : method.getParameterTypes())
				if(ReflectionHelper.getRootComponentClass(
						ptClass).getName().equals(excludedType))
					return (valid = false);
		}

		return (valid = true);
	}

	boolean valid()
	{
		return valid;
	}

	/**
	 * Build the return type and parameter types of this method as JavaTypes.
	 * If the JavaType creation for return type and parameter type succeeds,
	 * return true.
	 */
	protected void buildTypes()
	{
		Type rt = method.getGenericReturnType();
		Type[] ptArr = method.getGenericParameterTypes();

		if(rt instanceof TypeVariable)
		{
			Type returnType = javaType.getMatchingTypeArgument(rt);

			if(returnJavaType != null)
			{
				returnJavaType = JavaType.create(returnType);

				CGLogger.progressLog(method.toGenericString() + " called javaType.getMatchingGenericJavaType(" +
						rt.toString() + "), which returned " + returnJavaType.getTypeClass());
			}
			else
			{
				CGLogger.progressLog(method.toGenericString() + " called javaType.getMatchingGenericJavaType(" +
						rt.toString() + "), which returned null");
			}
		}
		else if(rt instanceof ParameterizedType)
		{
			ParameterizedType pmt = (ParameterizedType) rt;
			Type[] tar = pmt.getActualTypeArguments();
			Type[] matchingTypeArgs = tar != null ? new Type[tar.length] : null;

			for(int i = 0; tar != null && i < tar.length; i++)
			{
				Type matchingType = javaType.getMatchingTypeArgument(tar[i]);

				matchingTypeArgs[i] = matchingType != null ? matchingType : tar[i];
			}

			returnJavaType = JavaType.create(method.getReturnType(), matchingTypeArgs);
		}

		// common scenario
		if(returnJavaType == null) returnJavaType = JavaType.create(rt);

		for(int i = 0; i < ptArr.length; i++)
		{
			Type pt = ptArr[i];

			JavaType pJavaType = null;

			if(pt instanceof TypeVariable)
			{
				Type pType = javaType.getMatchingTypeArgument(pt);

				if(pType != null)
				{
					pJavaType = JavaType.create(pType);

					CGLogger.progressLog(method.toGenericString() + " called javaType.getMatchingGenericJavaType(" +
							pt.toString() + "), which returned " + pJavaType.getTypeClass());
				}
				else
				{
					CGLogger.progressLog(method.toGenericString() + " called javaType.getMatchingGenericJavaType(" +
							pt.toString() + "), which returned null");
				}
			}
			else if(pt instanceof ParameterizedType)
			{
				ParameterizedType pmt = (ParameterizedType) pt;
				Type[] tar = pmt.getActualTypeArguments();
				Type[] matchingTypeArgs = tar != null ? new Type[tar.length] : null;

				for(int j = 0; tar != null && j < tar.length; j++)
				{
					Type matchingType = javaType.getMatchingTypeArgument(tar[j]);

					matchingTypeArgs[j] = matchingType != null ? matchingType : tar[j];
				}

				pJavaType = JavaType.create(method.getParameterTypes()[i], matchingTypeArgs);
			}

			// common scenario
			if(pJavaType == null) pJavaType = JavaType.create(pt);

			parameterJavaTypes.add(pJavaType);
		}
	}

	public JavaType getJavaType()
	{
		return javaType;
	}

	public Method getMethod()
	{
		return method;
	}

	public JavaMethodType getMethodType()
	{
		return methodType;
	}

	public boolean isAccessor()
	{
		return ! methodType.equals(JavaMethodType.FUNCTION);
	}

	public boolean isGetter()
	{
		return methodType.equals(JavaMethodType.GETTER);
	}

	public boolean isSetter()
	{
		return methodType.equals(JavaMethodType.SETTER);
	}

	public boolean isQueryMethod()
	{
		return methodType.equals(JavaMethodType.QUERY);
	}

	public String getName()
	{
		return method.getName();
	}

	public String getId()
	{
		return this.id;
	}

	public JavaType getReturnJavaType()
	{
		return returnJavaType;
	}

	public List<JavaType> getParameterJavaTypes()
	{
		return parameterJavaTypes;
	}

	public Class<?> getTypeClass()
	{
		return javaType.getTypeClass();
	}

	public String getPropertyName()
	{
		return propertyName;
	}

	@Override
	public boolean equals(Object obj)
	{
		return obj != null && obj instanceof JavaMethod && ((JavaMethod) obj).toString().equals(toString());
	}

	@Override
	public String toString()
	{
		return this.method.toGenericString();
	}
}
